สำรวจคุณสมบัติ TypeScript ขั้นสูง เช่น template literal types และ conditional types เพื่อเขียนโค้ดที่แสดงออกได้และดูแลรักษาง่ายขึ้น ฝึกฝนการจัดการประเภทสำหรับสถานการณ์ที่ซับซ้อน
TypeScript ประเภทขั้นสูง: การเรียนรู้ Template Literal และ Conditional Types
จุดแข็งของ TypeScript อยู่ที่ระบบประเภทที่ทรงพลัง แม้ว่าประเภทพื้นฐาน เช่น string, number และ boolean จะเพียงพอสำหรับหลายสถานการณ์ แต่คุณสมบัติขั้นสูง เช่น template literal types และ conditional types จะปลดล็อกระดับการแสดงออกและความปลอดภัยของประเภทใหม่ คู่มือนี้จะให้ภาพรวมที่ครอบคลุมของประเภทขั้นสูงเหล่านี้ สำรวจความสามารถและแสดงตัวอย่างการใช้งานจริง
ทำความเข้าใจ Template Literal Types
Template literal types สร้างต่อยอดจาก template literals ของ JavaScript ทำให้คุณสามารถกำหนดประเภทตามการแทรกสตริง ซึ่งช่วยให้สามารถสร้างประเภทที่แสดงถึงรูปแบบสตริงเฉพาะ ทำให้โค้ดของคุณมีความทนทานและคาดเดาได้มากขึ้น
ไวยากรณ์พื้นฐานและการใช้งาน
Template literal types ใช้เครื่องหมาย backticks (`) เพื่อล้อมรอบนิยามประเภท คล้ายกับ JavaScript template literals ภายใน backticks คุณสามารถแทรกประเภทอื่น ๆ โดยใช้ไวยากรณ์ ${} ที่นี่คือจุดที่เวทมนตร์เกิดขึ้น – คุณกำลังสร้างประเภทที่เป็นสตริง ซึ่งถูกสร้างขึ้นในเวลาคอมไพล์ตามประเภทที่อยู่ภายใน interpolation
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/${string}`;
// ตัวอย่างการใช้งาน
const getEndpoint: APIEndpoint = "/api/users"; // ถูกต้อง
const postEndpoint: APIEndpoint = "/api/products/123"; // ถูกต้อง
const invalidEndpoint: APIEndpoint = "/admin/settings"; // TypeScript จะไม่แสดงข้อผิดพลาดที่นี่ เนื่องจาก `string` สามารถเป็นอะไรก็ได้
ในตัวอย่างนี้ APIEndpoint เป็นประเภทที่แสดงถึงสตริงใด ๆ ที่ขึ้นต้นด้วย /api/ แม้ว่าตัวอย่างพื้นฐานนี้จะมีประโยชน์ แต่พลังที่แท้จริงของ template literal types จะปรากฏเมื่อใช้ร่วมกับข้อจำกัดประเภทที่เฉพาะเจาะจงมากขึ้น
การรวมกับ Union Types
Template literal types จะเปล่งประกายอย่างแท้จริงเมื่อใช้กับ union types ซึ่งช่วยให้คุณสร้างประเภทที่แสดงถึงชุดค่าผสมสตริงเฉพาะ
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIPath = "users" | "products" | "orders";
type APIEndpoint = `/${APIPath}/${HTTPMethod}`;
// API Endpoints ที่ถูกต้อง
const getUsers: APIEndpoint = "/users/GET";
const postProducts: APIEndpoint = "/products/POST";
// API Endpoints ที่ไม่ถูกต้อง (จะส่งผลให้เกิดข้อผิดพลาด TypeScript)
// const invalidEndpoint: APIEndpoint = "/users/PATCH"; // Error: "/users/PATCH" ไม่สามารถกำหนดให้กับประเภท "/users/GET" | "/users/POST" | "/users/PUT" | "/users/DELETE" | "/products/GET" | "/products/POST" | ... อีก 3 ... | "/orders/DELETE" ได้
ตอนนี้ APIEndpoint เป็นประเภทที่เข้มงวดกว่าซึ่งอนุญาตเฉพาะการผสมผสานระหว่าง API path และ HTTP methods ที่เฉพาะเจาะจง TypeScript จะแจ้งเตือนความพยายามใด ๆ ที่ใช้การผสมผสานที่ไม่ถูกต้อง ซึ่งช่วยเพิ่มความปลอดภัยของประเภท
การจัดการสตริงด้วย Template Literal Types
TypeScript มี intrinsic string manipulation types ที่ทำงานร่วมกับ template literal types ได้อย่างราบรื่น ประเภทเหล่านี้ช่วยให้คุณแปลงสตริงในเวลาคอมไพล์
- Uppercase: แปลงสตริงเป็นตัวพิมพ์ใหญ่
- Lowercase: แปลงสตริงเป็นตัวพิมพ์เล็ก
- Capitalize: ขึ้นต้นคำด้วยตัวพิมพ์ใหญ่
- Uncapitalize: ขึ้นต้นคำด้วยตัวพิมพ์เล็ก
type Greeting = "hello world";
type UppercaseGreeting = Uppercase; // "HELLO WORLD"
type LowercaseGreeting = Lowercase; // "hello world"
type CapitalizedGreeting = Capitalize; // "Hello world"
type UncapitalizedGreeting = Uncapitalize; // "hello world"
ประเภทการจัดการสตริงเหล่านี้มีประโยชน์อย่างยิ่งสำหรับการสร้างประเภทโดยอัตโนมัติตามรูปแบบการตั้งชื่อ ตัวอย่างเช่น คุณสามารถสร้างประเภทแอ็คชันจากชื่อเหตุการณ์หรือกลับกัน
การใช้งานจริงของ Template Literal Types
- การกำหนด API Endpoint: ดังที่แสดงไว้ข้างต้น การกำหนด API endpoints ด้วยข้อจำกัดประเภทที่แม่นยำ
- การจัดการเหตุการณ์: การสร้างประเภทสำหรับชื่อเหตุการณ์ที่มีคำนำหน้าและคำต่อท้ายเฉพาะ
- การสร้าง CSS Class: การสร้างชื่อ CSS class ตามชื่อคอมโพเนนต์และสถานะ
- การสร้าง Database Query: การรับรองความปลอดภัยของประเภทเมื่อสร้าง database queries
ตัวอย่างสากล: การจัดรูปแบบสกุลเงิน
ลองนึกภาพการสร้างแอปพลิเคชันทางการเงินที่รองรับหลายสกุลเงิน คุณสามารถใช้ template literal types เพื่อบังคับการจัดรูปแบบสกุลเงินที่ถูกต้อง
type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";
type CurrencyFormat = `${number} ${T}`;
const priceUSD: CurrencyFormat<"USD"> = "100 USD"; // ถูกต้อง
const priceEUR: CurrencyFormat<"EUR"> = "50 EUR"; // ถูกต้อง
// const priceInvalid: CurrencyFormat<"USD"> = "100 EUR"; // Error: Type 'string' is not assignable to type '`${number} USD`'.
function formatCurrency(amount: number, currency: T): CurrencyFormat {
return `${amount} ${currency}`;
}
const formattedUSD = formatCurrency(250, "USD"); // Type: "250 USD"
const formattedEUR = formatCurrency(100, "EUR"); // Type: "100 EUR"
ตัวอย่างนี้รับประกันว่าค่าสกุลเงินจะถูกจัดรูปแบบด้วยรหัสสกุลเงินที่ถูกต้องเสมอ ซึ่งป้องกันข้อผิดพลาดที่อาจเกิดขึ้น
เจาะลึก Conditional Types
Conditional types นำตรรกะการแตกแขนงเข้าสู่ระบบประเภทของ TypeScript ทำให้คุณสามารถกำหนดประเภทที่ขึ้นอยู่กับประเภทอื่น คุณสมบัตินี้มีประสิทธิภาพอย่างยิ่งในการสร้างนิยามประเภทที่ยืดหยุ่นและนำกลับมาใช้ใหม่ได้สูง
ไวยากรณ์พื้นฐานและการใช้งาน
Conditional types ใช้คำหลัก infer และ ternary operator (condition ? trueType : falseType) เพื่อกำหนดเงื่อนไขประเภท
type IsString = T extends string ? true : false;
type StringCheck = IsString; // type StringCheck = true
type NumberCheck = IsString; // type NumberCheck = false
ในตัวอย่างนี้ IsString เป็น conditional type ที่ตรวจสอบว่า T สามารถกำหนดให้กับ string ได้หรือไม่ หากเป็นเช่นนั้น ประเภทจะถูกประมวลผลเป็น true มิฉะนั้นจะเป็น false
คำหลัก infer
คำหลัก infer ช่วยให้คุณสามารถดึงประเภทจากประเภทอื่นได้ ซึ่งมีประโยชน์อย่างยิ่งเมื่อทำงานกับประเภทที่ซับซ้อน เช่น function types หรือ array types
type ReturnType any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType; // type AddReturnType = number
ในตัวอย่างนี้ ReturnType จะดึง return type ของ function type T ส่วน infer R ของ conditional type จะอนุมาน return type และกำหนดให้กับตัวแปรประเภท R หาก T ไม่ใช่ function type ประเภทจะถูกประมวลผลเป็น any
Distributive Conditional Types
Conditional types จะกลายเป็น distributive เมื่อประเภทที่ตรวจสอบเป็น naked type parameter ซึ่งหมายความว่า conditional type จะถูกนำไปใช้กับแต่ละสมาชิกของ union type แยกต่างหาก
type ToArray = T extends any ? T[] : never;
type NumberOrStringArray = ToArray; // type NumberOrStringArray = string[] | number[]
ในตัวอย่างนี้ ToArray จะแปลงประเภท T เป็น array type เนื่องจาก T เป็น naked type parameter (ไม่ได้ถูกห่อหุ้มด้วยประเภทอื่น) conditional type จะถูกนำไปใช้กับ number และ string แยกกัน ทำให้ได้ union ของ number[] และ string[]
การใช้งานจริงของ Conditional Types
- การดึง Return Types: ดังที่แสดงไว้ข้างต้น การดึง return type ของฟังก์ชัน
- การกรองประเภทจาก Union: การสร้างประเภทที่มีเฉพาะประเภทที่ต้องการจาก union
- การสร้าง Overloaded Function Types: การสร้าง function types ที่แตกต่างกันตาม input types
- การสร้าง Type Guards: การสร้างฟังก์ชันที่จำกัดประเภทของตัวแปร
ตัวอย่างสากล: การจัดการรูปแบบวันที่ที่แตกต่างกัน
ภูมิภาคต่างๆ ทั่วโลกใช้รูปแบบวันที่ที่แตกต่างกัน คุณสามารถใช้ conditional types เพื่อจัดการความแตกต่างเหล่านี้ได้
type DateFormat = "YYYY-MM-DD" | "MM/DD/YYYY" | "DD.MM.YYYY";
type ParseDate = T extends "YYYY-MM-DD"
? { year: number; month: number; day: number; format: "YYYY-MM-DD" }
: T extends "MM/DD/YYYY"
? { month: number; day: number; year: number; format: "MM/DD/YYYY" }
: T extends "DD.MM.YYYY"
? { day: number; month: number; year: number; format: "DD.MM.YYYY" }
: never;
function parseDate(dateString: string, format: T): ParseDate {
// (Implementation จะจัดการกับรูปแบบวันที่ที่แตกต่างกัน)
if (format === "YYYY-MM-DD") {
const [year, month, day] = dateString.split("-").map(Number);
return { year, month, day, format } as ParseDate;
} else if (format === "MM/DD/YYYY") {
const [month, day, year] = dateString.split("/").map(Number);
return { month, day, year, format } as ParseDate;
} else if (format === "DD.MM.YYYY") {
const [day, month, year] = dateString.split(".").map(Number);
return { day, month, year, format } as ParseDate;
} else {
throw new Error("Invalid date format");
}
}
const parsedDateISO = parseDate("2023-10-27", "YYYY-MM-DD"); // Type: { year: number; month: number; day: number; format: "YYYY-MM-DD"; }
const parsedDateUS = parseDate("10/27/2023", "MM/DD/YYYY"); // Type: { month: number; day: number; year: number; format: "MM/DD/YYYY"; }
const parsedDateEU = parseDate("27.10.2023", "DD.MM.YYYY"); // Type: { day: number; month: number; year: number; format: "DD.MM.YYYY"; }
console.log(parsedDateISO.year); // เข้าถึง year โดยรู้ว่าจะมีอยู่
ตัวอย่างนี้ใช้ conditional types เพื่อกำหนดฟังก์ชันการแยกวิเคราะห์วันที่ที่แตกต่างกันตามรูปแบบวันที่ที่ระบุ ประเภท ParseDate รับประกันว่าออบเจกต์ที่ส่งคืนมีคุณสมบัติที่ถูกต้องตามรูปแบบ
การรวม Template Literal และ Conditional Types
พลังที่แท้จริงจะมาจากการรวม template literal types และ conditional types ซึ่งช่วยให้สามารถจัดการประเภทที่ทรงพลังอย่างเหลือเชื่อได้
type EventName = `on${Capitalize}`;
type ExtractEventPayload = T extends EventName
? { type: T; payload: any } // ทำให้ง่ายขึ้นเพื่อการสาธิต
: never;
type ClickEvent = EventName<"click">; // "onClick"
type MouseOverEvent = EventName<"mouseOver">; // "onMouseOver"
// ตัวอย่างฟังก์ชันที่รับประเภท
function processEvent(event: T): ExtractEventPayload {
// ในการใช้งานจริง เราจะส่ง event จริง
console.log(`Processing event ${event}`);
// ในการใช้งานจริง payload จะขึ้นอยู่กับประเภท event
return { type: event, payload: {} } as ExtractEventPayload;
}
// สังเกตว่า return types มีความเฉพาะเจาะจงมาก:
const clickEvent = processEvent("onClick"); // { type: "onClick"; payload: any; }
const mouseOverEvent = processEvent("onMouseOver"); // { type: "onMouseOver"; payload: any; }
// หากคุณใช้สตริงอื่น คุณจะได้ never:
// const someOtherEvent = processEvent("someOtherEvent"); // Type คือ `never`
แนวทางปฏิบัติและข้อควรพิจารณา
- ทำให้เรียบง่าย: แม้จะมีประสิทธิภาพ แต่ประเภทขั้นสูงเหล่านี้อาจซับซ้อนได้อย่างรวดเร็ว พยายามทำให้ชัดเจนและดูแลรักษาง่าย
- ทดสอบอย่างละเอียด: ตรวจสอบให้แน่ใจว่านิยามประเภทของคุณทำงานตามที่คาดหวังโดยการเขียน unit tests ที่ครอบคลุม
- เอกสารประกอบโค้ดของคุณ: เอกสารประกอบวัตถุประสงค์และพฤติกรรมของประเภทขั้นสูงของคุณอย่างชัดเจนเพื่อปรับปรุงความสามารถในการอ่านโค้ด
- พิจารณาประสิทธิภาพ: การใช้ประเภทขั้นสูงมากเกินไปอาจส่งผลต่อเวลาในการคอมไพล์ โปรไฟล์โค้ดของคุณและปรับให้เหมาะสมเมื่อจำเป็น
บทสรุป
Template literal types และ conditional types เป็นเครื่องมือที่ทรงพลังในคลังแสงของ TypeScript ด้วยการเรียนรู้ประเภทขั้นสูงเหล่านี้ คุณสามารถเขียนโค้ดที่แสดงออกได้ ดูแลรักษาง่าย และปลอดภัยประเภทมากขึ้น คุณสมบัติเหล่านี้ช่วยให้คุณจับความสัมพันธ์ที่ซับซ้อนระหว่างประเภท บังคับใช้ข้อจำกัดที่เข้มงวดขึ้น และสร้างนิยามประเภทที่นำกลับมาใช้ใหม่ได้สูง โอบรับเทคนิคเหล่านี้เพื่อยกระดับทักษะ TypeScript ของคุณและสร้างแอปพลิเคชันที่ทนทานและปรับขนาดได้สำหรับผู้ชมทั่วโลก